import { NextRequest, NextResponse } from "next/server"; import JSZip from "jszip"; import { prisma } from "@/lib/prisma"; import { loadFileData } from "@/lib/storage"; import { fileInShare, folderInShare, hasPasswordAccess, loadShare, shareInfo, shareStatusError, touchShare, type ShareWithTargets } from "@/lib/shareAccess"; function fileResponse(buffer: Buffer, name: string, mimeType: string, download: boolean) { return new NextResponse(new Uint8Array(buffer), { headers: { "Content-Type": download ? "application/octet-stream" : mimeType, "Content-Length": String(buffer.length), "Content-Disposition": `${download ? "attachment" : "inline"}; filename*=UTF-8''${encodeURIComponent(name)}`, "Cache-Control": "private, no-store", }, }); } async function addFolderToZip(zip: JSZip, folderId: string, prefix: string) { const files = await prisma.file.findMany({ where: { folderId, deletedAt: null } }); for (const file of files) { try { zip.file(prefix + file.name, await loadFileData(file.storagePath, file.isEncrypted)); } catch { /* fichier illisible : ignoré */ } } const subfolders = await prisma.folder.findMany({ where: { parentId: folderId } }); for (const sub of subfolders) await addFolderToZip(zip, sub.id, `${prefix}${sub.name}/`); } /** Listing d'un dossier du partage : fichiers actifs, sous-dossiers, fil d'Ariane borné à la racine partagée. */ async function folderListing(share: ShareWithTargets, folderId: string) { const folder = await prisma.folder.findUnique({ where: { id: folderId }, select: { id: true, name: true, parentId: true } }); if (!folder) return null; const [files, children] = await Promise.all([ prisma.file.findMany({ where: { folderId, deletedAt: null }, orderBy: { name: "asc" }, select: { id: true, name: true, mimeType: true, size: true, updatedAt: true } }), prisma.folder.findMany({ where: { parentId: folderId }, orderBy: { name: "asc" }, select: { id: true, name: true, _count: { select: { files: true, children: true } } } }), ]); const crumbs: { id: string; name: string }[] = []; let cur: { id: string; name: string; parentId: string | null } | null = folder; while (cur) { crumbs.unshift({ id: cur.id, name: cur.name }); if (cur.id === share.folder!.id) break; cur = cur.parentId ? await prisma.folder.findUnique({ where: { id: cur.parentId }, select: { id: true, name: true, parentId: true } }) : null; } return { folder: { id: folder.id, name: folder.name }, crumbs, folders: children.map((c) => ({ id: c.id, name: c.name, files: c._count.files, children: c._count.children })), files: files.map((f) => ({ ...f, size: Number(f.size) })), }; } export async function GET( request: NextRequest, { params }: { params: { token: string } } ) { const share = await loadShare(params.token); const err = shareStatusError(share); if (err || !share) return err!; const { searchParams } = new URL(request.url); const action = searchParams.get("action"); const wantContent = searchParams.get("content") === "true"; const fileId = searchParams.get("fileId"); const folderId = searchParams.get("folderId"); // Mot de passe non validé (cookie signé absent) : on ne révèle que le minimum if (!hasPasswordAccess(request, share)) { const info = shareInfo(share); return NextResponse.json({ requiresPassword: true, type: info.type, name: info.name, fileName: info.name, // compatibilité ancien client fileType: share.file?.mimeType ?? "inode/directory", mode: share.mode, expiresAt: info.expiresAt, }); } // ---- Partage de FICHIER ---- if (share.file) { if (wantContent || action === "download") { const buffer = await loadFileData(share.file.storagePath, share.file.isEncrypted); const download = action === "download" || share.mode === "DOWNLOAD"; await touchShare(share.id); return fileResponse(buffer, share.file.name, share.file.mimeType, download); } await touchShare(share.id); return NextResponse.json(shareInfo(share)); } // ---- Partage de DOSSIER ---- const root = share.folder!; if (fileId) { const f = await fileInShare(share, fileId); if (!f) return NextResponse.json({ error: "Fichier hors du partage" }, { status: 404 }); if (wantContent || action === "download") { const buffer = await loadFileData(f.storagePath, f.isEncrypted); await touchShare(share.id); return fileResponse(buffer, f.name, f.mimeType, action === "download" || share.mode === "DOWNLOAD"); } return NextResponse.json({ id: f.id, name: f.name, mimeType: f.mimeType, size: Number(f.size), mode: share.mode }); } // ZIP du dossier partagé (ou d'un sous-dossier) if (action === "download") { const target = folderId ?? root.id; if (!(await folderInShare(share, target))) return NextResponse.json({ error: "Dossier hors du partage" }, { status: 404 }); const folder = await prisma.folder.findUnique({ where: { id: target }, select: { name: true } }); const zip = new JSZip(); await addFolderToZip(zip, target, ""); const buffer = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE", compressionOptions: { level: 6 } }); await touchShare(share.id); return new NextResponse(new Uint8Array(buffer), { headers: { "Content-Type": "application/zip", "Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent((folder?.name || root.name) + ".zip")}`, "Cache-Control": "private, no-store", }, }); } // Listing (racine du partage ou sous-dossier) const target = folderId ?? root.id; if (!(await folderInShare(share, target))) return NextResponse.json({ error: "Dossier hors du partage" }, { status: 404 }); const listing = await folderListing(share, target); if (!listing) return NextResponse.json({ error: "Dossier introuvable" }, { status: 404 }); await touchShare(share.id); return NextResponse.json({ ...shareInfo(share), ...listing }); }